Descriptors let objects customize attribute lookup, storage, and deletion.
以上是Python Documentation對描述器的解釋,也是最清楚、最直觀的解釋。直翻的話就是「描述器允許物件自訂屬性的查找、儲存和刪除行為」。
描述器可以想像成有特定功能的一個類別物件,它基本包含了__get__
、__set__
、__delete__
三個方法對應上述提到的查找、儲存和刪除。
class MyDescriptor: # 描述器
def __get__(self, instance, instanceType=None):
return "Hello World"
class MyClass:
attr = MyDescriptor()
obj = MyClass()
print(obj.attr) # Output: Hello World
當編譯器執行obj.attr
時,他實際上會跑attr.__get__(obj, MyClass)
,所以會回傳Hello World
回來。
呼叫__get__
method以取得attribute的值。 這個方法除了self外還會有兩個參數:
如果__get__
是由類別呼叫,則instance
為None.
加上 __set__
來編輯 attribute:
class MyDescriptor: # 描述器
def __init__(self):
self._name = "Hello World"
def __get__(self, instance, instancetype=None):
return self._name
def __set__(self, instance, value):
if isinstance(value, str):
self._name = value
else:
raise ValueError("Name must be a string")
class MyClass:
attr = MyDescriptor()
obj = MyClass()
print(obj.attr) # Output: Hello World
obj.attr = "Good Night"
print(obj.attr) # Output: Good Night
obj.attr = 123 # ValueError: Name must be a string
從這個案例中可以看到描述器的第一個好處:對attribute的設定增加自己的邏輯。
以obj.attr = "Good Night"
為例,實際執行的流程為attr.__set__(obj, "Good Night")
。這時候我們自己加了輸入值屬性的檢測,因此obj.attr = 123
的123就會因為屬性不合被擋住。
再加上__delete__
:
class MyDescriptor: # 描述器
def __init__(self):
self._name = "Hello World"
def __get__(self, instance, instancetype=None):
return self._name
def __set__(self, instance, value):
if isinstance(value, str):
self._name = value
else:
raise ValueError("Name must be a string")
def __delete__(self, instance):
raise AttributeError("This attribute cannot be deleted")
class MyClass:
attr = MyDescriptor()
obj = MyClass()
del obj.attr # AttributeError: "This attribute cannot be deleted"
編譯器執行del obj.attr
時,實際上執行att.__delete__(obj)
。而這裡設定__delete__
會丟出一個錯誤,並不會真的刪掉目標attribute。
只有__get__
的為non-data descriptor。除__get__
還有__set__
或__delete__
的為data descriptor。
當我們呼叫一個attribute時,編譯器會按照此實體的__mro__
找尋相應的值。
MRO 代表 Method Resolution Order,是python 中類別決定attrubute值為何的順序。
AttributeError
。以下為例:
class A:
def __getattr__(self, name):
return f"{name} not found in A, but handled by __getattr__"
class B(A):
dd_1 = 123 # Regular class attribute
def __init__(self):
self.instance_attr = "Instance attribute in B"
b = B()
print(b.dd_1) # Directly from B's class dictionary
print(b.instance_attr) # From instance dictionary of b
print(b.some_random_attr) # Handled by __getattr__ of class A
將描述器拆出後,不但可以將attribute的管理邏輯封裝,需要時還可以重複使用這個邏輯。在維護與事後調整上也較清楚容易。
描述器很適合拿來延遲計算某些數值,直到被呼叫到時再計算,而不是先計算好然後佔住部分記憶體。
沒有用描述器的範例:
class DataAnalysis:
def __init__(self, data):
self.data = data
self._result = self._analyze_data()
def _analyze_data(self):
# Simulate a time-consuming analysis
return sum(self.data) / len(self.data)
# 答案在實體化時就會被計算好,並儲存下來。就算之後沒用到也一樣。
有用描述器的範例:
class LazyProperty:
def __init__(self, function):
self.function = function
self.attribute_name = f"_{function.__name__}"
def __get__(self, obj, objtype=None):
if not hasattr(obj, self.attribute_name):
setattr(obj, self.attribute_name, self.function(obj))
return getattr(obj, self.attribute_name)
class DataAnalysis:
def __init__(self, data):
self.data = data
@LazyProperty
def result(self):
# Simulate a time-consuming analysis
return sum(self.data) / len(self.data)
analysis = DataAnalysis([10, 20, 30, 40])
print(analysis.result) # result 這時候被呼叫到,計算才會開始並回傳。由於__get__中有setattr,因此會同時將答覆存在obj的__dict__中,成為一個新的實體attribute。
print(analysis.result) # result 再次被呼叫。但實體__dict__內現在有result,因此不用再次計算,可以直接拿到答案。
如同上面提過的範例,我們可以在描述器內檢測輸入值是否符合型別要求。
如以下:
class TypeChecked:
def __init__(self, expected_type, attribute_name):
self.expected_type = expected_type
self.attribute_name = attribute_name
def __set__(self, obj, value):
if not isinstance(value, self.expected_type):
raise TypeError(f"{self.attribute_name} must be of type {self.expected_type}")
obj.__dict__[self.attribute_name] = value
class Person:
name = TypeChecked(str, 'name')
age = TypeChecked(int, 'age')
# 這樣可以保證 Attributes 的型別與大小符合我們的想像。之後也比較不會有bug。
常見的裝飾器classmethod
、staticmethod
都是描述器的一種,而且是non-data descriptor。不過兩者實際上都是用C寫得。假如純用Python來寫得話,classmethod
會長這樣:
import functools
class ClassMethod:
"Emulate PyClassMethod_Type() in Objects/funcobject.c"
def __init__(self, f):
self.f = f
functools.update_wrapper(self, f)
def __get__(self, obj, cls=None):
if cls is None:
cls = type(obj)
if hasattr(type(self.f), '__get__'):
# This code path was added in Python 3.9
# and was deprecated in Python 3.11.
return self.f.__get__(cls, cls)
return MethodType(self.f, cls)